//俄罗斯方块游戏
#include<stdio.h>
#include<graphics.h>
#include<time.h>
#include<conio.h>
int score = 0;
int rank = 0;
#define BLOCK_COUNT 7
#define BLOCK_WIDTH 5
#define BLOCK_HEIGHT 5

#define UNIT_SIZE 20 //小方块宽度

#define START_X 130		//方块降落框，方块降落起始位置
#define START_Y 30

#define KEY_UP 87		//用户操作
#define KEY_LEFT 65
#define KEY_RIGHT 68
#define KEY_DOWN 83
#define KEY_SPACE 32

#define MinX 30		//游戏左上角位置
#define MinY 30
int speed = 500;	//方块降落速度


int NextIndex = -1;		//下一个方块
int BlockIndex = -1;		//当前方块

typedef enum { //方块朝向
	BLOCK_UP,
	BLOCK_RIGHT,
	BLOCK_LEFT,
	BLOCK_DOWN
}block_dir_t;

typedef enum { //方块移动方向
	MOVE_DOwN,
	MOVE_LEFT,
	MOVE_RIGHT
}move_dir_t;

//方块颜色
int color[BLOCK_COUNT] = {
	GREEN,
	CYAN,
	MAGENTA,
	YELLOW,
	BROWN
};
int visit[30][15];//访问数组visit[i][j]=1表示该位置有方块
int marColor[30][15];//对应位置颜色

//开始界面，绘制画布
void welcome() {
	initgraph(600, 700);//初始化画布，创建一个宽600像素，高700像素的窗口
	HWND window = GetHWnd();//获得当前窗口
	SetWindowText(window, _T("俄罗斯方块，tetris"));//设置窗口标题
	setfont(60, 0, _T("黑体"));//设置文本的字体样式(高，宽（0表示自适应），字体）
	setcolor(WHITE);//设置字体颜色
	outtextxy(150, 250, _T("俄罗斯方块"));//设置字开始从哪里出现
	Sleep(4000);//表示暂停4秒后继续执行代码
}

//游戏内界面
void initgamescreen() {
	char str[16];//创建一个存放分数的字符串数组
	cleardevice();//用于清除当前图形设备上的所有内容，为绘制新的游戏场景做准备
	rectangle(50, 50, 355, 655);//用于绘制方块降落外框
	rectangle(52, 52, 353, 653);//绘制方块降落内框
	rectangle(380, 70, 525, 215);//绘制方块提示框
	setfont(20, 0, _T("楷体"));
	setcolor(GREEN);
	outtextxy(420, 230,_T("下一个："));
	//绘制输出分数
	setcolor(RED);
	outtextxy(420, 280, _T("分数"));
	sprintf_s(str, 16, "%d", score);//按照指定格式将score写入str
	outtextxy(430, 310, str);//这里设置字符集为多字符，保证outtextxy可以写出变量str
	outtextxy(420, 375, _T("等级："));
	sprintf_s(str, 16, "%d", rank);//将rank变量的值转化为字符串并储存在str数组中
	outtextxy(430, 405, str); //输出存储在 str 数组中的等级值。
	setcolor(LIGHTBLUE);
	outtextxy(400, 475, "操作说明");
	outtextxy(400, 500, "pgup：旋转");
	outtextxy(400, 525, "pgdn：下降");
	outtextxy(400, 550, "home：左移");
	outtextxy(400, 575, "end：右移");
	outtextxy(400, 600, "空格：暂停");
}
//方块表示
int block[BLOCK_COUNT * 4][BLOCK_WIDTH][BLOCK_HEIGHT] = {
	//|形方块
	{ 0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0 },
	{ 0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0 },
	{ 0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0 },
	{ 0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0 },

	//L形方块
	{ 0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,1,0,0,0,0,0,0 },
	{ 0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,1,0,0,0,0,0,0,0,0 },
	{ 0,0,0,0,0,0,1,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0 },
	{ 0,0,0,0,0,0,0,0,1,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0 },

	//T形方块
	{ 0,0,0,0,0,0,0,1,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0 },
	{ 0,0,0,0,0,0,0,1,0,0,0,0,1,1,0,0,0,1,0,0,0,0,0,0,0 },
	{ 0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,1,0,0,0,0,0,0,0 },
	{ 0,0,0,0,0,0,0,1,0,0,0,1,1,0,0,0,0,1,0,0,0,0,0,0,0 },

	//Z形方块
	{ 0,0,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0 },
	{ 0,0,0,0,0,0,0,1,0,0,0,1,1,0,0,0,1,0,0,0,0,0,0,0,0 },
	{ 0,0,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0 },
	{ 0,0,0,0,0,0,0,1,0,0,0,1,1,0,0,0,1,0,0,0,0,0,0,0,0 },

	//J形方块
	{ 0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,1,1,0,0,0,0,0,0,0 },
	{ 0,0,0,0,0,0,1,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0 },
	{ 0,0,0,0,0,0,0,1,1,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0 },
	{ 0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,1,0,0,0,0,0,0 },

	//S形方块
	{ 0,0,0,0,0,0,0,1,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0 },
	{ 0,0,0,0,0,0,0,1,0,0,0,0,1,1,0,0,0,0,1,0,0,0,0,0,0 },
	{ 0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,1,1,0,0,0,0,0,0,0 },
	{ 0,0,0,0,0,0,1,0,0,0,0,1,1,0,0,0,0,1,0,0,0,0,0,0,0 },

	//田形方块
	{ 0,0,0,0,0,0,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0 },
	{ 0,0,0,0,0,0,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0 },
	{ 0,0,0,0,0,0,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0 },
	{ 0,0,0,0,0,0,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0 },

};
//新方块表示，清空方块提示框里的方块
void clearBlock() {
	setcolor(BLACK);
	setfont(23, 0, "楷体");
	//遍历到每一个方框，确定每个方框的位置
	for (int i = 0; i < BLOCK_HEIGHT; i++) {
		for (int j = 0; j < BLOCK_WIDTH; j++) {
			int x = 401 + j * UNIT_SIZE;
			int y = 91 + i * UNIT_SIZE;
			//由于之前设置了颜色为黑色，所以输出的黑色方块会覆盖原有的方块，从而实现清空提示框的效果。
			outtextxy(x, y, "██");
		}
	}
}

//清除降落过程的方块
void clearBlock(int x, int y, block_dir_t blockDir) {
	setcolor(BLACK);
	//	setfont(23, 0, "楷体");
	int id = BlockIndex * 4 + blockDir;
	for (int i = 0; i < BLOCK_HEIGHT; ++i) {
		for (int j = 0; j < BLOCK_WIDTH; ++j) {
			if (block[id][i][j] == 1) {
				int x2 = x + j * UNIT_SIZE;
				int y2 = y + i * UNIT_SIZE;
				outtextxy(x2, y2, "■");
			}
		}
	}
}

//在提示框和降落框的起始位置画方块
void drawBlock(int x,int y) {
	setcolor(color[NextIndex]);//获取对应方块的颜色
	setfont(23, 0, "楷体");
	for (int i = 0; i < BLOCK_HEIGHT; i++) {
		for (int j = 0; j < BLOCK_WIDTH; j++) {
			//检查 block 三维数组中对应位置的值是否为 1
			//如果该位置的值为 1，表示该位置有方块需要绘制
			if (block[NextIndex * 4][i][j] == 1) {
				int x2 = x + j * UNIT_SIZE;
				int y2 = y + i * UNIT_SIZE;
				outtextxy(x2, y2, "██");//绘制图案
			}
		}
	}
}
//绘制下落过程的方块
void drawBlock(int x, int y, block_dir_t dir) {
	setcolor(color[BlockIndex]);
	setfont(23, 0, "楷体");
	int id = BlockIndex * 4 + dir;
	for (int i = 0; i < BLOCK_HEIGHT; ++i) {
		for (int j = 0; j < BLOCK_WIDTH; ++j) {
			if (block[id][i][j] == 1) {
				//擦除该方块的第i行第j列
				outtextxy(x + j * UNIT_SIZE, y + i * UNIT_SIZE, "■");
			}
		}
	}
}

//在方块提示框中产生新的方块
void nextblock() {
	clearBlock();//清除提示框的方块
	srand((unsigned)time(NULL));//产生随机数，随机选择方块
//rand 函数用于生成随机数，但它实际上是一个伪随机数生成器，需要一个随机种子来初始化。
//srand 函数用于设置随机种子，这里使用 time(NULL) 的返回值作为随机种子，time(NULL) 返回当前的时间戳，
//由于时间是不断变化的，所以每次程序运行时的随机种子都不同，从而可以生成不同的随机数序列。
	NextIndex = rand() % BLOCK_COUNT;//产生0~BLOCK_COUNT-1的随机数
	drawBlock(401, 91);
}

//判断方块是否可以按照指定位置移动
bool moveable(int x0, int y0, move_dir_t moveDir, block_dir_t blockDir) {
	//MinY,MinX使游戏区域左上角的像素坐标
	//通过 (y0 - MinY) / UNIT_SIZE 计算出方块左上角在游戏区的行索引 x
	//通过 (x0 - MinX) / UNIT_SIZE 计算出方块左上角在游戏区的列索引 y
	int x = (y0 - MinY) / UNIT_SIZE;
	int y = (x0 - MinX) / UNIT_SIZE;
	int ret = 1;//初始化一个变量，将其作为方块可以移动的初始假设
	int id = BlockIndex * 4 + blockDir;//当前方块的编号，由于每个方块有 4 种旋转状态，
	//所以通过 BlockIndex * 4 + blockDir 计算出当前方块在 block 三维数组中的具体索引 id
	if (moveDir == MOVE_DOwN) {
		for (int i = 0; i < BLOCK_HEIGHT; i++) {
			for (int j = 0; j < BLOCK_WIDTH; j++) {
				if (block[id][i][j] == 1 && (x + 1 + i == 30 || visit[x + i + 1][y + j] == 1))
					ret = 0;
			}
		}
	}
	else if (moveDir == MOVE_LEFT) {
		for (int i = 0; i < BLOCK_HEIGHT; i++) {
			for (int j = 0; j < BLOCK_WIDTH; j++) {
				if (block[id][i][j] == 1 && (y + j <= 0 || visit[x + i][y + j - 1] == 1))
					ret = 0;
			}
		}
	}
	else if (moveDir == MOVE_RIGHT) {
		for (int i = 0; i < BLOCK_HEIGHT; i++) {
			for (int j = 0; j < BLOCK_WIDTH; j++) {
				if (block[id][i][j] == 1 && (y + 1 + j >= 15 || visit[x + i][y + j + 1] == 1))
					ret = 0;
			}
		}
	}
	return ret;
}

//检查游戏是否结束
void failCheck() {
	if (!moveable(START_X, START_Y, MOVE_DOwN, (block_dir_t)BLOCK_UP)) {
		setcolor(WHITE);
		setfont(45,0,"隶体");
		outtextxy(75,300,"游戏结束！");
		Sleep(3000);
		system("pause");
		closegraph();
		exit(0);
	}
}
//延时等待
void wait(int interval) {
	int count = interval / 10;
	for (int i = 0; i < count; i++) {
		Sleep(10);
		if (_kbhit()) {
			return;
		}
	}
}
//判断当前方块是否可以向指定方向旋转
bool rotatable(int x, int y, block_dir_t dir) {
	//首先判断是否可以继续向下移动
	if (!moveable(x, y, MOVE_DOwN, dir)) {
		return false;
	}
	int x2 = (y - MinY) / UNIT_SIZE;
	int y2 = (x - MinX) / UNIT_SIZE;
	int id = BlockIndex * 4 + dir;
	//判断左右是否有阻挡，或者越界情况
	for (int i = 0; i < BLOCK_HEIGHT; i++) {
		for (int j = 0; j < BLOCK_WIDTH; j++) {
			if (block[id][i][j] == 1 && (y2 + j < 0 || y2 + j >= 15 || visit[x2 + i][y2 + j] == 1))
				return false;
		}
	}
	return true;
}
//固定方块，如果方块不能移动，说明它需要固化
void mark(int x, int y, block_dir_t dir) {
	int id = BlockIndex * 4 + dir;
	int x2 = (y - MinY) / UNIT_SIZE;
	int y2 = (x - MinX) / UNIT_SIZE;
	for (int i = 0; i < BLOCK_HEIGHT; i++) {
		for (int j = 0; j < BLOCK_WIDTH; j++) {
			if (block[id][i][j] == 1) {
				visit[x2 + i][y2 + j] = 1;
				marColor[x2 + i][y2 + j] = color[BlockIndex];
			}
		}
	}
}
//读取用户操作，实时更新降落的方块
void move() {
	int x = START_X;
	int y = START_Y;
	int k = 0;
	block_dir_t blockDir = (block_dir_t)BLOCK_UP;
	int curSpeed = speed;
	failCheck();
	while (1) {
		int curSpeed = speed;
		clearBlock(x, k + y, blockDir);
		if (_kbhit()) {
			int key = _getch();
			if (key == KEY_SPACE)
				system("pause");
			else if (key == KEY_UP) {
				block_dir_t nextDir = (block_dir_t)((blockDir + 1) % 4);
				if (rotatable(x, y + k, nextDir)) {
					blockDir = nextDir;
				}
			}
			else if (key == KEY_LEFT) {
				if (moveable(x, y + k + 20, MOVE_LEFT, blockDir)) {
					x -= UNIT_SIZE;
				}
			}
			else if (key == KEY_RIGHT) {
				if (moveable(x, y + k + 20, MOVE_RIGHT, blockDir)) {
					x += UNIT_SIZE;
				}
			}
			else if (key == KEY_DOWN) {
				curSpeed = 50;
			}
			k += 20;
			drawBlock(x, y + k, blockDir);
			wait(curSpeed);
			//方块的固化处理，方块固定后结束循环，当前一个方块的move执行完毕
			if (!moveable(x, y + k, MOVE_DOwN, blockDir)) {
				mark(x, y + k, blockDir);
				break;
			}
		}
	}
}
//更新提示框的方块
void newblock() {
	BlockIndex = NextIndex;
	drawBlock(START_X, START_Y);
	Sleep(200);
	nextblock();
	move();
	}
//消除第i行，并把上面的行都往下移

void down(int x) {
	for (int i = x; i > 0; --i) {
		for (int j = 0; j < 15; ++j) {
			if (visit[i - 1][j] == 1) {
				visit[i][j] = 1;
				marColor[i][j] = marColor[i - 1][j];
				setcolor(marColor[i][j]);
				outtextxy(20 * j + MinX, 20 * i + MinY, "■");
			}
			else {
				visit[i][j] = 0;
				setcolor(BLACK);
				outtextxy(20 * j + MinX, 20 * i + MinY, "■");
			}
		}
	}
	//清除最顶层方格
	setcolor(BLACK);
	for (int j = 0; j < 15; ++j) {
		visit[0][j] = 0;
		outtextxy(20 * j + MinX, MinY, "■");
	}
}

//更新分数
void addScore(int lines) {
	char str[32];
	score += lines * 10;
	sprintf_s(str, 32, "%d", score);
	setcolor(RED);
	outtextxy(415, 310, str);

}
//更新等级
void updateGrade() {
	//更新等级
	//假设50分一级
	rank = score / 50;
	char str[32];
	sprintf_s(str, 32, "%d", rank);
	setcolor(RED);
	outtextxy(415, 405, str);
	//更新速度
	if (speed <= 100) {
		speed = 100;
	}
	else {
		speed = 500 - rank * 20;
	}
}

//检查是否有满行方块
void check() {
	int i, j;
	int clearLines = 0;
	for (i = 29; i >= 0; i--) {
		// 检查第i行有没有满
		for (j = 0; j < 15 && visit[i][j]; j++);
		//执行到此处时，有两种情况：
		// 1. 第i行没有满，即表示有空位 此时 j<15
		// 2. 第i行已满了，此时 j>=15
		if (j >= 15) {
			// 此时，第i行已经满了，就需要消除第i行
			down(i);  //消除第i行，并把上面的行都下移
			i++;  // 因为最外层的循环中有 i--, 所以我们先i++, 使得下次循环时，再把这一行检查一下
			clearLines++;
		}
	}
	// 更新分数
	addScore(clearLines);

	// 更新等级(更新等级提示，更新速度）
	updateGrade();
}


	int main() {
		welcome();
		initgamescreen();
		nextblock();
		Sleep(800);
		memset(visit, 0, sizeof(visit));
		while (1) {
			newblock();
		}
		system("pause");
		closegraph();
		return 0;
}

